# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import annotations

import contextlib
import datetime as dt
import logging
from typing import TYPE_CHECKING, Iterator, Optional, cast

from typing_extensions import override

from crossbench.action_runner.action.action_type import ActionType
from crossbench.action_runner.action.dump_html import DumpHtmlAction
from crossbench.action_runner.action.get import GetAction
from crossbench.benchmarks.loading.page.base import Page
from crossbench.benchmarks.loading.playback_controller import \
    PlaybackController
from crossbench.benchmarks.loading.tab_controller import TabController
from crossbench.runner.probe_context_lookup_error import \
    ProbeContextLookupError

if TYPE_CHECKING:
  from crossbench.action_runner.base import ActionRunner
  from crossbench.benchmarks.loading.config.blocks import ActionBlock
  from crossbench.benchmarks.loading.config.login.custom import LoginBlock
  from crossbench.browsers.browser import Browser
  from crossbench.cli.config.secrets import Secrets
  from crossbench.runner.run import Run
  from crossbench.types import JsonDict


class InteractivePage(Page):

  def __init__(self,
               name: str,
               blocks: tuple[ActionBlock, ...],
               login: Optional[LoginBlock] = None,
               setup: Optional[ActionBlock] = None,
               teardown: Optional[ActionBlock] = None,
               secrets: Optional[Secrets] = None,
               playback: PlaybackController = PlaybackController.default(),
               tabs: TabController = TabController.default(),
               about_blank_duration: dt.timedelta = dt.timedelta(),
               run_login: bool = True,
               run_setup: bool = True,
               run_teardown: bool = True) -> None:
    assert name, "missing name"
    self._name: str = name
    assert not any(block.is_login for block in blocks), (
        "No login blocks allowed as normal action block")
    self._login_block: LoginBlock | None = login
    self._setup_block: ActionBlock | None = setup
    assert isinstance(blocks, tuple)
    self._blocks: tuple[ActionBlock, ...] = blocks
    assert self._blocks, "Must have at least 1 valid action"
    self._teardown_block: ActionBlock | None = teardown

    self._run_login: bool = run_login
    self._run_setup: bool = run_setup
    self._run_teardown: bool = run_teardown

    duration = self._get_duration()
    super().__init__(self._name, duration, playback, tabs, about_blank_duration,
                     secrets)

  @property
  def login_block(self) -> ActionBlock | None:
    return self._login_block

  @property
  def setup_block(self) -> ActionBlock | None:
    return self._setup_block

  @property
  def blocks(self) -> tuple[ActionBlock, ...]:
    return self._blocks

  @property
  def teardown_block(self) -> ActionBlock | None:
    return self._teardown_block

  @property
  @override
  def first_url(self) -> str:
    for block in self.blocks:
      for action in block:
        if action.TYPE == ActionType.GET:
          return cast(GetAction, action).url
    raise RuntimeError("No GET action with an URL found.")

  def create_failure_artifacts(self,
                               run: Run,
                               message: str = "failure") -> None:
    action_runner = run.action_runner
    try:
      action_runner.failure_screenshot(run, message)
    except Exception as e:  # noqa: BLE001
      logging.error("Failed to take a failure screenshot: %s", e)

    try:
      action_runner.invoke_probe(run, DumpHtmlAction(suffix=message))
    except ProbeContextLookupError:
      pass
    except Exception as e:  # noqa: BLE001
      logging.error("Failed to dump HTML on failure: %s", e)

  @contextlib.contextmanager
  def _performance_mark_scope(self, run: Run, name: str) -> Iterator[None]:
    browser: Browser = run.browser
    browser.performance_mark(f"{name}-start", self._name)
    yield
    browser.performance_mark(f"{name}-end", self._name)

  @override
  def setup(self, run: Run) -> None:
    action_runner = run.action_runner
    if self._run_login and (login_block := self.login_block):
      with self._performance_mark_scope(run, "login"):
        action_runner.run_login(run, self, login_block)
    if self._run_setup and (setup_block := self.setup_block):
      with self._performance_mark_scope(run, "setup"):
        action_runner.run_setup(run, self, setup_block)

  @override
  def teardown(self, run: Run) -> None:
    action_runner = run.action_runner
    if self._run_teardown and (teardown_block := self.teardown_block):
      with self._performance_mark_scope(run, "teardown"):
        action_runner.run_teardown(run, self, teardown_block)
    action_runner.teardown()

  def run_once(self, run: Run) -> None:
    action_runner = run.action_runner
    multiple_tabs = self.tabs.multiple_tabs
    action_runner.run_interactive_page(run, self, multiple_tabs)

  @override
  def run_with(self, run: Run, action_runner: ActionRunner,
               multiple_tabs: bool) -> None:
    action_runner.run_interactive_page(run, self, multiple_tabs)

  @override
  def details_json(self) -> JsonDict:
    result = super().details_json()
    result["actions"] = [block.to_json() for block in self._blocks]
    return result

  def _get_duration(self) -> dt.timedelta:
    duration = dt.timedelta()
    for block in self._blocks:
      duration += block.duration
    return duration
